import { eventType, TOUCH_EVENT, style, offset, preventDefaultException } from '../util/dom'
import { ease } from '../util/ease'
import { momentum } from '../util/momentum'
import { requestAnimationFrame, cancelAnimationFrame } from '../util/raf'
import { PROBE_REALTIME, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_UP, DIRECTION_DOWN } from '../util/const'

export function coreMixin (GScroll) {
  GScroll.prototype._start = function (e) {
    const _eventType = eventType[e.type]
    if (_eventType !== TOUCH_EVENT) {
      // 当e.button === 0，表示用户点击鼠标左键
      if (e.button !== 0) {
        return
      }
    }
    if (!this.enabled || (this.initiated && this.initiated !== _eventType)) {
      return
    }
    this.initiated = _eventType

    if (this.options.preventDefault && !preventDefaultException(e.target, this.options.preventDefaultException)) {
      e.preventDefault()
    }
    if (this.options.stopPropagation) {
      e.stopPropagation()
    }

    // 滚动后的距离
    this.distX = 0
    this.distY = 0
    // 滚动后的方向
    this.directionX = 0
    this.directionY = 0
    // 处于移动中的方向
    this.movingDirectionX = 0
    this.movingDirectionY = 0
    // 锁定滚动方向
    this.diretionLocked = 0

    this._transitionTime()
    this.startTime = new Date()

    this.stop()

    let point = e.touches ? e.touches[0] : e

    // 滚动开始的位置
    this.startX = this.x
    this.startY = this.y
    // 滚动开始的位置，只有start事件会改变值
    this.constStartX = this.x
    this.constStartY = this.y
    // 当前点的位置
    this.pointX = point.pageX
    this.pointY = point.pageY
  }

  GScroll.prototype._move = function (e) {
    if (!this.enabled || eventType[e.type] !== this.initiated) {
      return
    }

    if (this.options.preventDefault) {
      e.preventDefault()
    }
    if (this.options.stopPropagation) {
      e.stopPropagation()
    }

    let point = e.touches ? e.touches[0] : e
    let deltaX = point.pageX - this.pointX
    let deltaY = point.pageY - this.pointY

    this.pointX = point.pageX
    this.pointY = point.pageY

    this.distX += deltaX
    this.distY += deltaY

    // 滚动后的绝对值距离
    let absDistX = Math.abs(this.distX)
    let absDistY = Math.abs(this.distY)

    let timestamp = new Date()

    // 至少移动momentumLimitDistance像素才能开始滚动，避免滚动距离太短而引起的抖动
    if (
      timestamp - this.endTime > this.options.momentumLimitTime &&
      (absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance)
    ) {
      return
    }

    // 如果向一个方向滚动，则锁定另一个方向
    if (!this.diretionLocked && !this.options.freeScroll) {
      if (absDistX > absDistY + this.options.diretionLockThreshold) {
        this.diretionLocked = 'h' // 锁定水平方向
      } else if (absDistY >= absDistX + this.options.diretionLockThreshold) {
        this.diretionLocked = 'v' // 锁定垂直方向
      } else {
        this.diretionLocked = 'n' // 无锁，任意方向
      }
    }

    if (this.diretionLocked === 'h') {
      if (this.options.eventPassThrough === 'vertical') {
        e.preventDefault()
      } else if (this.options.eventPassThrough === 'horizontal') {
        this.initiated = false
        return
      }
      this.deltaY = 0
    } else if (this.diretionLocked === 'v') {
      if (this.options.eventPassThrough === 'horizontal') {
        e.preventDefault()
      } else if (this.options.eventPassThrough === 'vertical') {
        this.initiated = false
        return
      }
      this.deltaX = 0
    }

    deltaX = this.hasHorizontalScroll ? deltaX : 0
    deltaY = this.hasVerticalScroll ? deltaY : 0
    this.movingDirectionX = deltaX > 0 ? DIRECTION_RIGHT : deltaX < 0 ? DIRECTION_LEFT : 0
    this.movingDirectionY = deltaY > 0 ? DIRECTION_DOWN : deltaY < 0 ? DIRECTION_UP : 0

    // 滚动后的位置
    let newX = this.x + deltaX
    let newY = this.y + deltaY

    let top = false
    let bottom = false
    let left = false
    let right = false
    // 如果超出边界，减速或者停止
    const bounce = this.options.bounce
    if (bounce !== false) {
      top = bounce.top === undefined ? true : bounce.top
      bottom = bounce.bottom === undefined ? true : bounce.bottom
      left = bounce.left === undefined ? true : bounce.left
      right = bounce.right === undefined ? true : bounce.right
    }
    if (newX > this.minScrollX || newX < this.maxScrollX) {
      if ((newX > this.minScrollX && left) || (newX < this.maxScrollX && right)) {
        newX = this.x + deltaX / 3
      } else {
        newX = newX > this.minScrollX ? this.minScrollX : this.maxScrollX
      }
    }
    if (newY > this.minScrollY || newY < this.maxScrollY) {
      if ((newY > this.minScrollY && top) || (newY < this.maxScrollY && bottom)) {
        newY = this.y + deltaY / 3
      } else {
        newY = newY > this.minScrollY ? this.minScrollY : this.maxScrollY
      }
    }

    this._translate(newX, newY)

    // 记录滚动的最新位置
    if (timestamp - this.startTime > this.options.momentumLimitTime) {
      this.startTime = timestamp
      this.startX = this.x
      this.startY = this.y
    }

    // 实时监听滚动事件并回调
    if (this.options.probeType === PROBE_REALTIME) {
      this._startProbe()
    }

    this.trigger('scroll', {
      x: newX,
      y: newY
    })

    let scrollLeft = document.documentElement.scrollLeft || window.pageXOffset || document.body.scrollLeft
    let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop

    let pX = this.pointX - scrollLeft
    let pY = this.pointY - scrollTop

    // 超出边界就直接触发end事件
    if (
      pX > document.documentElement.clientWidth - this.options.momentumLimitDistance ||
      pX < this.options.momentumLimitDistance ||
      pY > document.documentElement.clientHeight - this.options.momentumLimitDistance ||
      pY < this.options.momentumLimitDistance
    ) {
      this._end(e)
    }
  }

  GScroll.prototype._end = function (e) {
    if (!this.enabled || eventType[e.type] !== this.initiated) {
      return
    }
    this.initiated = false

    if (this.options.preventDefault && !preventDefaultException(e.target, this.options.preventDefaultException)) {
      e.preventDefault()
    }
    if (this.options.stopPropagation) {
      e.stopPropagation()
    }

    this.isInTransition = false

    // 确保最后一个位置是最接近的整数
    let newX = Math.round(this.x)
    let newY = Math.round(this.y)

    let deltaX = newX - this.constStartX
    let deltaY = newY - this.constStartY
    this.directionX = deltaX > 0 ? DIRECTION_RIGHT : deltaX < 0 ? DIRECTION_LEFT : 0
    this.directionY = deltaY > 0 ? DIRECTION_DOWN : deltaY < 0 ? DIRECTION_UP : 0

    // 如果是下拉刷新就不执行后续滚动
    if (this.options.pullDownRefresh && this._checkPullDown()) {
      return
    }

    // 超出边界，重置位置
    if (this.resetPosition(this.options.bounceTime, ease.bounce)) {
      return
    }

    this._translate(newX, newY)

    this.endTime = new Date()
    let duration = this.endTime - this.startTime
    let absDistX = Math.abs(newX - this.startX)
    let absDistY = Math.abs(newY - this.startY)

    let time = 0
    // 条件符合就开始滚动惯性动画
    if (
      this.options.momentum &&
      duration < this.options.momentumLimitTime &&
      (absDistX > this.options.momentumLimitDistance || absDistY > this.options.momentumLimitDistance)
    ) {
      let top = false
      let bottom = false
      let left = false
      let right = false
      const bounce = this.options.bounce
      if (bounce !== false) {
        top = bounce.top === undefined ? true : bounce.top
        bottom = bounce.bottom === undefined ? true : bounce.bottom
        left = bounce.left === undefined ? true : bounce.left
        right = bounce.right === undefined ? true : bounce.right
      }
      const wrapperWidth = (this.directionX === DIRECTION_RIGHT && left) || (this.directionX === DIRECTION_LEFT && right) ? this.wrapperWidth : 0
      const wrapperHeight = (this.directionY === DIRECTION_DOWN && top) || (this.directionY === DIRECTION_UP && bottom) ? this.wrapperHeight : 0
      let momentumX = this.hasHorizontalScroll ? momentum(this.x, this.startX, duration, this.maxScrollX, this.minScrollX, wrapperWidth, this.options, this)
        : { destination: newX, duration: 0 }
      let momentumY = this.hasVerticalScroll ? momentum(this.y, this.startY, duration, this.maxScrollY, this.minScrollY, wrapperHeight, this.options, this)
        : { destination: newY, duration: 0 }
      newX = this.hasHorizontalScroll ? momentumX.destination : 0
      newY = this.hasVerticalScroll ? momentumY.destination : 0
      time = Math.max(momentumX.duration, momentumY.duration)
      this.isInTransition = true
    }

    let easing = ease.swipe

    if (newX !== this.x || newY !== this.y) {
      // 超出边界，改变滚动动画效果
      if (newX > this.minScrollX || newX < this.maxScrollX || newY > this.minScrollY || newY < this.maxScrollY) {
        easing = ease.swipeBounce
      }
      this.scrollTo(newX, newY, time, easing)
    }

    // 如果是轮播，就滚动到附近的视图
    if (this.options.snap) {
      let snap = this._nearestSnap(newX, newY)
      newX = snap.x
      newY = snap.y
      easing = ease.swipeBounce
      let time = 300
      this.scrollTo(newX, newY, time, easing)
    }

    this.trigger('scrollEnd', {
      x: newX,
      y: newY
    })
  }

  GScroll.prototype._transitionTime = function (time = 0) {
    this.scrollerStyle[style.transitionDuration] = time + 'ms'
  }

  GScroll.prototype._transitionTimingFunction = function (easing) {
    this.scrollerStyle[style.transitionTimingFunction] = easing
  }

  GScroll.prototype._translate = function (x, y) {
    x = this.hasHorizontalScroll ? x : 0
    y = this.hasVerticalScroll ? y : 0

    this.scrollerStyle[style.transform] = `translate(${x}px, ${y}px) translateZ(0)`

    this.x = x
    this.y = y

    this.isInTransition = true
  }

  GScroll.prototype._transitionEnd = function (e) {
    if (this.isInTransition) {
      this.resetPosition()
      this.isInTransition = false
    }

    this.trigger('transitionEnd', {
      x: this.x,
      y: this.y
    })

    this.trigger('scrollEnd', {
      x: this.x,
      y: this.y
    })
  }

  GScroll.prototype.resetPosition = function () {
    let needReset = false
    let easing = ease.bounce
    let newX = Math.round(this.x)
    let newY = Math.round(this.y)

    if (newX > this.minScrollX || newY > this.minScrollY) {
      newX = this.minScrollX
      newY = this.minScrollY
      needReset = true
    }
    if (newX < this.maxScrollX || newY < this.maxScrollY) {
      newX = this.maxScrollX
      newY = this.maxScrollY
      needReset = true
    }

    // 下拉刷新就不执行重置位置
    if (!this.pullingDown && needReset) {
      newX = this.hasHorizontalScroll ? newX : 0
      newY = this.hasVerticalScroll ? newY : 0
      this.scrollTo(newX, newY, this.options.bounceTime, easing)

      this.trigger('scrollEnd', {
        x: newX,
        y: newY
      })
    }

    this.refresh()

    return needReset
  }

  GScroll.prototype.scrollTo = function (x, y, time, easing = ease.bounce) {
    this.isInTransition = this.options.useTransition && time > 0 && (x !== this.x || y !== this.y)

    if (!time || this.options.useTransition) {
      this._transitionTimingFunction(easing.style)
      this._transitionTime(time)
      this._translate(x, y)

      if (time && this.options.probeTypeType === PROBE_REALTIME) {
        this._startProbe()
      }
    }
  }

  GScroll.prototype.scrollToElement = function (el, time, offsetX = 0, offsetY = 0, easing = ease.bounce) {
    if (!el) {
      return
    }

    el = el.nodeType ? el : this.scroller.querySelector(el)

    let pos = offset(el)
    pos.left -= this.wrapperOffset.left
    pos.top -= this.wrapperOffset.top
    pos.left = pos.left > this.minScrollX ? this.minScrollX : pos.left < this.maxScrollX ? this.maxScrollX : pos.left
    pos.top = pos.top > this.minScrollY ? this.minScrollY : pos.top < this.maxScrollY ? this.maxScrollY : pos.top

    pos.left += offsetX
    pos.top += offsetY

    this.scrollTo(pos.left, pos.top, time, easing)
  }

  GScroll.prototype._startProbe = function () {
    let me = this

    cancelAnimationFrame(this.probeTimer)
    this.probeTimer = requestAnimationFrame(probe)

    function probe () {
      let pos = me.getComputedPosition()
      me.trigger('scroll', pos)
      if (!me.isInTransition) {
        me.trigger('scrollEnd', pos)
        return
      }
      me.probeTimer = requestAnimationFrame(probe)
    }
  }

  GScroll.prototype.getComputedPosition = function () {
    let transform = getComputedStyle(this.scroller)['transform']
    let matrix = transform.replace(/[^\d-.,]/g, '').split(',')

    let newX = Number(matrix[4])
    let newY = Number(matrix[5])

    return {
      x: newX,
      y: newY
    }
  }

  GScroll.prototype.stop = function () {
    let pos = this.getComputedPosition()

    if (!this.pullingDown && this.resetPosition()) {
      return
    }

    if (this.isInTransition) {
      this.isInTransition = false
    }

    cancelAnimationFrame(this.probeTimer)

    if (pos.x || pos.y) {
      let newX = this.hasHorizontalScroll ? pos.x : 0
      let newY = this.hasVerticalScroll ? pos.y : 0
      this._translate(newX, newY)

      this.trigger('scrollEnd', {
        x: newX,
        y: newY
      })
    }
  }
}
